/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.ohosannotations.internal.core.helper;

import org.ohosannotations.ElementValidation;
import org.ohosannotations.annotations.DataBound;
import org.ohosannotations.annotations.EAbility;
import org.ohosannotations.annotations.EFraction;
import org.ohosannotations.annotations.EViewGroup;
import org.ohosannotations.annotations.Receiver;
import org.ohosannotations.annotations.Trace;
import org.ohosannotations.annotations.UiThread;
import org.ohosannotations.annotations.sharedpreferences.DefaultBoolean;
import org.ohosannotations.annotations.sharedpreferences.DefaultFloat;
import org.ohosannotations.annotations.sharedpreferences.DefaultInt;
import org.ohosannotations.annotations.sharedpreferences.DefaultLong;
import org.ohosannotations.annotations.sharedpreferences.DefaultString;
import org.ohosannotations.annotations.sharedpreferences.SharedPref;
import org.ohosannotations.api.sharedpreferences.SharedPreferencesHelper;
import org.ohosannotations.helper.CanonicalNameConstants;
import org.ohosannotations.helper.IdAnnotationHelper;
import org.ohosannotations.helper.IdValidatorHelper;
import org.ohosannotations.helper.OhosManifest;
import org.ohosannotations.helper.TargetAnnotationHelper;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;

import static java.util.Arrays.asList;
import static org.ohosannotations.helper.ModelConstants.classSuffix;

/**
 * 核心验证器辅助
 *
 * @author dev
 * @since 2021-07-22
 */
public class CoreValidatorHelper extends IdValidatorHelper {
    private static final List<String> VALID_PREF_RETURN_TYPES
        = Arrays.asList("int", "boolean", "float", "long",
        CanonicalNameConstants.STRING, CanonicalNameConstants.STRING_SET);
    private static final List<String> INVALID_PREF_METHOD_NAMES
        = Arrays.asList("edit", "getSharedPreferences", "clear", "getEditor", "apply");

    private static final List<Receiver.RegisterAt> VALID_ABILITY_REGISTER_AT
        = Arrays.asList(Receiver.RegisterAt.OnCreateOnDestroy, Receiver.RegisterAt.OnResumeOnPause,
        Receiver.RegisterAt.OnStartOnStop);
    private static final List<Receiver.RegisterAt> VALID_SERVICE_REGISTER_AT
        = Collections.singletonList(Receiver.RegisterAt.OnCreateOnDestroy);
    private static final List<Receiver.RegisterAt> VALID_FRAGMENT_REGISTER_AT
        = Arrays.asList(Receiver.RegisterAt.OnCreateOnDestroy, Receiver.RegisterAt.OnResumeOnPause,
        Receiver.RegisterAt.OnStartOnStop, Receiver.RegisterAt.OnAttachOnDetach);
    private static final List<Receiver.RegisterAt> VALID_VIEW_REGISTER_AT
        = Collections.singletonList(Receiver.RegisterAt.OnAttachOnDetach);

    /**
     * 核心验证
     *
     * @param idAnnotationHelper 验证ID
     */
    public CoreValidatorHelper(IdAnnotationHelper idAnnotationHelper) {
        super(idAnnotationHelper);
    }

    /**
     * ReturnValue
     *
     * @param executableElement executableElement
     * @param valid valid
     */
    public void doesNotHaveTraceAnnotationAndReturnValue(ExecutableElement executableElement, ElementValidation valid) {
        TypeMirror returnType = executableElement.getReturnType();
        if (elementHasAnnotation(Trace.class, executableElement) && returnType.getKind() != TypeKind.VOID) {
            valid.addError(executableElement, "@WakeLock annotated methods with a return value are not supported by @Trace");
        }
    }

    /**
     * PartialWakeLock
     *
     * @param element element
     * @param valid valid
     */
    public void doesNotUseFlagsWithPartialWakeLock(Element element, ElementValidation valid) {
    }

    /**
     * applicationRegistered
     *
     * @param element 元素
     * @param manifest 表明
     * @param valid 有效
     */
    public void applicationRegistered(Element element, OhosManifest manifest, ElementValidation valid) {
        if (manifest.isLibraryProject()) {
            return;
        }

        String applicationClassName = manifest.getApplicationClassName();
        if (applicationClassName != null) {
            TypeElement typeElement = (TypeElement) element;
            String componentQualifiedName = typeElement.getQualifiedName().toString();
            String generatedComponentQualifiedName = componentQualifiedName + classSuffix();

            if (!typeElement.getModifiers().contains(Modifier.ABSTRACT)
                && !applicationClassName.equals(generatedComponentQualifiedName)) {
                if (applicationClassName.equals(componentQualifiedName)) {
                    valid.addError("The OhosConfig.json file contains the "
                        + "original component, and not the OhosAnnotations generated component." + " Please register "
                        + generatedComponentQualifiedName + " instead of " + componentQualifiedName);
                } else {
                    valid.addWarning("The component "
                        + generatedComponentQualifiedName + " is not registered in the OhosConfig.json file.");
                }
            }
        } else {
            valid.addError("No application class registered in the OhosConfig.json");
        }
    }

    /**
     * isSharedPreference
     *
     * @param element 元素
     * @param valid 有效
     */
    public void isSharedPreference(Element element, ElementValidation valid) {
        TypeMirror type = element.asType();
        if (element instanceof ExecutableElement) {
            element = ((ExecutableElement) element).getParameters().get(0);
            type = element.asType();
        }

        /*
         * The type is not available yet because it has just been generated
         */
        if (type instanceof ErrorType || type.getKind() == TypeKind.ERROR) {
            String elementTypeName = type.toString();

            boolean sharedPrefValidatedInRound = false;
            if (elementTypeName.endsWith(classSuffix())) {
                String prefTypeName = elementTypeName
                    .substring(0, elementTypeName.length() - classSuffix().length());
                prefTypeName = prefTypeName.replace(classSuffix() + ".", ".");
                Set<? extends Element> sharedPrefElements = validatedModel()
                    .getRootAnnotatedElements(SharedPref.class.getName());
                for (Element sharedPrefElement : sharedPrefElements) {
                    TypeElement sharedPrefTypeElement = (TypeElement) sharedPrefElement;
                    String sharedPrefQualifiedName = sharedPrefTypeElement.getQualifiedName().toString();
                    if (sharedPrefQualifiedName.endsWith(prefTypeName)) {
                        sharedPrefValidatedInRound = true;
                        break;
                    }
                }
            }

            if (!sharedPrefValidatedInRound) {
                valid.invalidate();
            }
        } else {
            extendsType(element, SharedPreferencesHelper.class.getName(), valid);
        }
    }

    /**
     * isPrefMethod
     *
     * @param element 元素
     * @param valid 有效
     */
    public void isPrefMethod(Element element, ElementValidation valid) {
        if (!element.getKind().equals(ElementKind.METHOD)) {
            valid.addError("Only methods are allowed in an %s annotated interface");
        } else {
            ExecutableElement executableElement = (ExecutableElement) element;
            String methodName = executableElement.getSimpleName().toString();
            if (executableElement.getParameters().size() > 0) {
                valid.addError("Method " + methodName + " should have no parameters in an %s annotated interface");
            } else {
                String returnType = executableElement.getReturnType().toString();
                if (!VALID_PREF_RETURN_TYPES.contains(returnType)) {
                    valid.addError("Method " + methodName
                        + " should only return preference simple types in an %s annotated interface");
                } else {
                    if (INVALID_PREF_METHOD_NAMES.contains(methodName)) {
                        valid.addError("The method name " + methodName + " is forbidden in an %s annotated interface");
                    } else {
                        return;
                    }
                }
            }
        }
        valid.invalidate();
    }

    /**
     * faultAnnotation
     *
     * @param method 方法
     * @param valid 有效
     */
    public void hasCorrectDefaultAnnotation(ExecutableElement method, ElementValidation valid) {
        checkDefaultAnnotation(method, DefaultBoolean.class, "boolean",
            new TypeKindAnnotationCondition(TypeKind.BOOLEAN), valid);
        checkDefaultAnnotation(method, DefaultFloat.class, "float",
            new TypeKindAnnotationCondition(TypeKind.FLOAT), valid);
        checkDefaultAnnotation(method, DefaultInt.class, "int",
            new TypeKindAnnotationCondition(TypeKind.INT), valid);
        checkDefaultAnnotation(method, DefaultLong.class, "long",
            new TypeKindAnnotationCondition(TypeKind.LONG), valid);
        checkDefaultAnnotation(method, DefaultString.class, "String",
            new DefaultAnnotationCondition() {
                @Override
                public boolean correctReturnType(TypeMirror returnType) {
                    return returnType.toString().equals(CanonicalNameConstants.STRING);
                }
            }, valid);
    }

    /**
     * Condition
     */
    private interface DefaultAnnotationCondition {
        boolean correctReturnType(TypeMirror returnType);
    }

    /**
     * Condition
     */
    private static class TypeKindAnnotationCondition implements DefaultAnnotationCondition {
        private final TypeKind typeKind;

        TypeKindAnnotationCondition(TypeKind typeKind) {
            this.typeKind = typeKind;
        }

        /**
         * correctReturnType
         *
         * @param returnType 返回类型
         * @return returnType
         */
        @Override
        public boolean correctReturnType(TypeMirror returnType) {
            return returnType.getKind() == typeKind;
        }
    }

    /**
     * Annotation
     *
     * @param method 方法
     * @param annotationClass 注释阶级
     * @param expectedReturnType 返回类型
     * @param condition 条件
     * @param valid 有效
     * @param <T> 类型
     */
    private <T extends Annotation> void checkDefaultAnnotation(ExecutableElement method, Class<T> annotationClass,
        String expectedReturnType, DefaultAnnotationCondition condition, ElementValidation valid) {
        T defaultAnnotation = method.getAnnotation(annotationClass);
        if (defaultAnnotation != null) {
            if (!condition.correctReturnType(method.getReturnType())) {
                valid.addError(TargetAnnotationHelper.annotationName(annotationClass)
                    + " can only be used on a method that returns a " + expectedReturnType);
            }
        }
    }

    /**
     * MethodParameters
     *
     * @param executableElement 可执行元素
     * @param valid 有效的
     */
    public void hasOnTextUpdatedMethodParameters(ExecutableElement executableElement,
        ElementValidation valid) {
        List<? extends VariableElement> parameters = executableElement.getParameters();
        boolean stringParameterFound = false;
        boolean textViewParameterFound = false;
        for (VariableElement parameter : parameters) {
            String parameterType = parameter.asType().toString();
            if (parameterType.equals(CanonicalNameConstants.STRING)) {
                if (stringParameterFound) {
                    valid.addError("Unrecognized parameter declaration. you "
                        + "can declare only one parameter of type java.lang.String");
                }
                stringParameterFound = true;
                continue;
            }
            if (parameterType.equals(CanonicalNameConstants.TEXT_VIEW)) {
                if (textViewParameterFound) {
                    valid.addError("Unrecognized parameter declaration. you can "
                        + "declare only one parameter of type ohos.agp.components.Text");
                }
                textViewParameterFound = true;
                continue;
            }
            if (parameter.asType().getKind() == TypeKind.INT || CanonicalNameConstants
                .INTEGER.equals(parameterType)) {
                String parameterName = parameter.toString();
                if ("start".equals(parameterName) || "before".equals(parameterName)
                    || "count".equals(parameterName)) {
                    continue;
                }
                valid.addError("Unrecognized parameter name. You can only have start, before, or count parameter name."
                    + " Try to pick a parameter from ohos.agp.components."
                    + "Text.TextObserver.onTextUpdated() method.");
                continue;
            }
            valid.addError("Unrecognized parameter (" + parameter.toString()
                + "). %s can only have a ohos.agp.components.Text parameter and/or "
                + "parameters from ohos.agp.components.Text.TextObserver.onTextUpdated() method.");
        }
    }

    /**
     * MethodParameters
     *
     * @param executableElement 可执行元素
     * @param valid 有效的
     */
    public void hasSliderProgressUpdatedMethodParameters
    (ExecutableElement executableElement, ElementValidation valid) {
        List<? extends VariableElement> parameters = executableElement.getParameters();
        boolean sliderParameterFound = false;
        boolean fromUserParameterFound = false;
        boolean progressParameterFound = false;
        for (VariableElement parameter : parameters) {
            String parameterType = parameter.asType().toString();
            if (parameterType.equals(CanonicalNameConstants.SLIDER)) {
                if (sliderParameterFound) {
                    valid.addError("Unrecognized parameter declaration. "
                        + "You can declare only one parameter of type " + CanonicalNameConstants.SLIDER);
                }
                sliderParameterFound = true;
                continue;
            }
            if (parameter.asType().getKind() == TypeKind.INT
                || CanonicalNameConstants.INTEGER.equals(parameterType)) {
                if (progressParameterFound) {
                    valid.addError("You can have only one parameter of type "
                        + CanonicalNameConstants.INTEGER);
                }
                progressParameterFound = true;
                continue;
            }
            if (parameter.asType().getKind() == TypeKind.BOOLEAN
                || CanonicalNameConstants.BOOLEAN.equals(parameterType)) {
                if (fromUserParameterFound) {
                    valid.addError("You can have only one parameter of type "
                        + CanonicalNameConstants.BOOLEAN);
                }
                fromUserParameterFound = true;
                continue;
            }
            valid.addError("Unrecognized parameter '" + parameter.toString()
                + "'. %s signature should be " + executableElement.getSimpleName()
                + "(" + CanonicalNameConstants.SLIDER
                + " slider, int progress, boolean fromUser). The 'fromUser' and 'progress' parameters are optional.");
        }
    }

    /**
     * Parameters
     *
     * @param executableElement 可执行元素
     * @param valid 有效的
     */
    public void hasSliderTouchMethodParameters(ExecutableElement executableElement, ElementValidation valid) {
        List<? extends VariableElement> parameters = executableElement.getParameters();

        if (parameters.size() > 1) {
            valid.addError("Unrecognized parameter declaration. You can only have one parameter of type "
                + CanonicalNameConstants.SLIDER + ". Try declaring " + executableElement.getSimpleName()
                + "(" + CanonicalNameConstants.SLIDER + " slider);");
            return;
        }

        if (parameters.size() == 1) {
            String parameterType = parameters.get(0).asType().toString();
            if (!parameterType.equals(CanonicalNameConstants.SLIDER)) {
                valid.addError("Unrecognized parameter declaration. You can only have one parameter of type "
                    + CanonicalNameConstants.SLIDER + ". Try declaring " + executableElement.getSimpleName()
                    + "(" + CanonicalNameConstants.SLIDER + " slider);");
            }
        }
    }

    /**
     * EnclosingElement
     *
     * @param element 元素
     * @param valid 有效的
     */
    public void hasRightRegisterAtValueDependingOnEnclosingElement(Element element, ElementValidation valid) {
        Element enclosingElement = element.getEnclosingElement();
        Receiver.RegisterAt registerAt = element.getAnnotation(Receiver.class).registerAt();

        Map<String, List<Receiver.RegisterAt>> validRegisterAts = new HashMap<>();
        validRegisterAts.put(CanonicalNameConstants.ABILITY, VALID_ABILITY_REGISTER_AT);
        validRegisterAts.put(CanonicalNameConstants.SERVICE, VALID_SERVICE_REGISTER_AT);
        validRegisterAts.put(CanonicalNameConstants.FRACTION, VALID_FRAGMENT_REGISTER_AT);
        validRegisterAts.put(CanonicalNameConstants.COMPONENT, VALID_VIEW_REGISTER_AT);

        for (Map.Entry<String, List<Receiver.RegisterAt>> validRegisterAt : validRegisterAts.entrySet()) {
            String enclosingType = validRegisterAt.getKey();
            Collection<Receiver.RegisterAt> validRegisterAtValues = validRegisterAt.getValue();
            if (extendsType(enclosingElement, enclosingType) && !validRegisterAtValues.contains(registerAt)) {
                valid.addError("The parameter registerAt of @Receiver in " + enclosingType
                    + " can only be one of the following values : " + validRegisterAtValues);
            }
        }
    }

    /**
     * IfLocal
     *
     * @param element 元素
     * @param valid 有效的
     */
    public void hasSupportV4JarIfLocal(Element element, ElementValidation valid) {
        boolean local = element.getAnnotation(Receiver.class).local();
        if (local) {
            Elements elementUtils = annotationHelper.getElementUtils();
            if (elementUtils.getTypeElement(CanonicalNameConstants.LOCAL_BROADCAST_MANAGER)
                == null && elementUtils.getTypeElement(CanonicalNameConstants
                .OHOSX_LOCAL_BROADCAST_MANAGER) == null) {
                valid.addError("To use the LocalBroadcastManager, you MUST " +
                    "include the ohos-support-v4 or ohosx.localbroadcastmanager jar");
            }
        }
    }

    /**
     * IfHasId
     *
     * @param element 元素
     * @param valid 有效的
     */
    public void usesEnqueueIfHasId(Element element, ElementValidation valid) {
        UiThread annotation = element.getAnnotation(UiThread.class);

        if (!(annotation.id() == -1) && annotation.propagation() == UiThread.Propagation.REUSE) {
            valid.addError("An id only can be used with Propagation.ENQUEUE");
        }
    }

    /**
     * EventCallback
     *
     * @param element 元素
     * @param validation 确认
     */
    public void extendsKeyEventCallback(Element element, ElementValidation validation) {
        extendsType(element, CanonicalNameConstants.KEY_EVENT_CALLBACK, validation);
    }

    /**
     * EventCallback
     *
     * @param element 元素
     * @param validation 确认
     */
    public void enclosingElementExtendsKeyEventCallback
    (Element element, ElementValidation validation) {
        extendsKeyEventCallback(element.getEnclosingElement(), validation);
    }

    /**
     * ClassIsFragment
     *
     * @param element 元素
     * @param validation 确认
     */
    public void childFragmentUsedOnlyIfEnclosingClassIsFragment
    (Element element, ElementValidation validation) {
        boolean childFragment = annotationHelper
            .extractAnnotationParameter(element, "childFraction");

        if (childFragment) {
            TypeElement fragment = annotationHelper.getElementUtils()
                .getTypeElement(CanonicalNameConstants.FRACTION);
            TypeElement supportFragment = annotationHelper.getElementUtils()
                .getTypeElement(CanonicalNameConstants.SUPPORT_V4_FRACTION);
            TypeElement ohosxFragment = annotationHelper.getElementUtils()
                .getTypeElement(CanonicalNameConstants.OHOSX_FRACTION);

            boolean enclosingElementIsFragment = false;

            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

            if (fragment != null && annotationHelper.isSubtype(enclosingElement, fragment)) {
                enclosingElementIsFragment = true;
            } else if (supportFragment != null && annotationHelper
                .isSubtype(enclosingElement, supportFragment)) {
                enclosingElementIsFragment = true;
            } else if (ohosxFragment != null && annotationHelper
                .isSubtype(enclosingElement, ohosxFragment)) {
                enclosingElementIsFragment = true;
            } else {
            }

            if (!enclosingElementIsFragment) {
                validation.addError(element, "The 'childFractionManager' " +
                    "parameter only can be used if the class containing the annotated field is either subclass of "
                    + CanonicalNameConstants.FRACTION + ", " + CanonicalNameConstants.SUPPORT_V4_FRACTION
                    + " or " + CanonicalNameConstants.OHOSX_FRACTION);
            }
        }
    }

    /**
     * IsAvailable
     *
     * @param element 元素
     * @param validation 确认
     */
    public void getChildFragmentManagerMethodIsAvailable(Element element, ElementValidation validation) {
        boolean childFragment = annotationHelper
            .extractAnnotationParameter(element, "childFraction");

        if (childFragment) {
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

            TypeElement fragment = annotationHelper.getElementUtils()
                .getTypeElement(CanonicalNameConstants.FRACTION);
            TypeElement supportFragment = annotationHelper.getElementUtils()
                .getTypeElement(CanonicalNameConstants.SUPPORT_V4_FRACTION);
            TypeElement ohosxFragment = annotationHelper.getElementUtils()
                .getTypeElement(CanonicalNameConstants.OHOSX_FRACTION);

            if (supportFragment != null && annotationHelper.isSubtype(enclosingElement, supportFragment)) {
                if (!methodIsAvailableIn(supportFragment, "getChildFractionManager")) {
                    validation.addError(element, "The 'childFractionManager' parameter " +
                        "only can be used if the getChildFractionManager() method is available in "
                        + CanonicalNameConstants.SUPPORT_V4_FRACTION + ", update your support library version.");
                }
            } else if (ohosxFragment != null && annotationHelper.isSubtype(enclosingElement, ohosxFragment)) {
                if (!methodIsAvailableIn(ohosxFragment, "getChildFractionManager")) {
                    validation.addError(element, "The 'childFragmentManager' parameter only can "
                        + "be used if the getChildFractionManager() method is available in "
                        + CanonicalNameConstants.OHOSX_FRACTION + ", update your support library version.");
                }
            } else if (fragment != null && annotationHelper.isSubtype(enclosingElement, fragment)
                && environment().getOhosManifest().getMinSdkVersion() < 17) {
                validation.addError(element, "The 'childFractionManager' parameter only can" +
                    " be used if the getChildFractionManager() method is available in "
                    + CanonicalNameConstants.FRACTION
                    + " (from API 17). Increment 'minSdkVersion' or use "
                    + CanonicalNameConstants.SUPPORT_V4_FRACTION + " or "
                    + CanonicalNameConstants.OHOSX_FRACTION + ".");
            }
        }
    }

    /**
     * AvailableIn
     *
     * @param element 元素
     * @param methodName 方法名字
     * @return false
     */
    private boolean methodIsAvailableIn(TypeElement element, String methodName) {
        for (Element method : ElementFilter.methodsIn(element.getEnclosedElements())) {
            if (method.getSimpleName().contentEquals(methodName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * BoundAnnotation
     *
     * @param element 元素
     * @param validation 确认
     */
    public void checkDataBoundAnnotation(Element element, ElementValidation validation) {
        if (element.getAnnotation(DataBound.class) != null
            && !isClassPresent(CanonicalNameConstants.DATA_BINDING_UTIL)
            && !isClassPresent(CanonicalNameConstants.OHOSX_DATA_BINDING_UTIL)) {
            validation.invalidate();
        }
    }

    /**
     * Classpath
     *
     * @param validation 确认
     */
    public void hasDataBindingOnClasspath(ElementValidation validation) {
        if (!isClassPresent(CanonicalNameConstants.DATA_BINDING_UTIL)
            && !isClassPresent(CanonicalNameConstants.OHOSX_DATA_BINDING_UTIL)) {
            validation.addError("Data binding is not found on classpath, " +
                "be sure to enable data binding for the project.");
        }
    }

    /**
     * EViewGroup
     *
     * @param element 元素
     * @param reportElement 元素报告
     * @param valid 有效的
     */
    public void hasEAbilityOrEFractionOrEViewGroup(Element element, Element reportElement, ElementValidation valid) {
        List<Class<? extends Annotation>> validAnnotations = asList(EAbility.class, EFraction.class, EViewGroup.class);
        hasOneOfAnnotations(reportElement, element, validAnnotations, valid);
    }

    /**
     * BoundAnnotation
     *
     * @param element 元素
     * @param validation 确认
     */
    public void enclosingElementHasDataBoundAnnotation(Element element, ElementValidation validation) {
        enclosingElementHasAnnotation(DataBound.class, element, validation);
    }
}
